/*
 * Licensed to the Tai Ping Jin Ke
 *
 * Copyright (c) 2022 .
 * All rights reserved.
 * 项目名称：太平金科-基础组件封装层-会话管理-reactor-缺省实现
 * 版权说明：本软件属太平金融科技服务(上海)有限公司所有，在未获得太平金融科技服务(上海)有限公司正式授权情况下，任何企业和个人，不能获取、阅读、安装、传播本软件涉及的任何受知识产权保护的内容。
 */
package com.taipingframework.boot.reactor.session.filters;

import com.taipingframework.boot.cache.redis.StandardCacheKeyBuilder;
import com.taipingframework.boot.session.ReactorSessionManager;
import com.taipingframework.boot.session.constant.SessionCachePathEnum;
import com.taipingframework.boot.session.constant.SessionConstant;
import com.taipingframework.utility.constant.ApplicationConstant;
import com.taipingframework.utility.web.properties.FluxFilterProperty;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.lang.NonNull;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.WebSession;
import reactor.core.publisher.Mono;

/**
 * 两件事：
 * （1）将会话标识添加到ServerWebExchange对象属性列表中；
 * （2）刷新会话对象在redis中的缓存。
 * <p>
 * 一个问题：
 * 在HTTP请求头中只设置字段`HeaderWebSessionIdResolver.headerName`，值为空字符串。
 * 此处情况下，执行`exchange.getSession()`抛出校验异常`sessionId must not be empty`。
 * <p>
 * 补充说明：
 * 在HTTP请求头的字段参数列表中，不设置`HeaderWebSessionIdResolver.headerName`所表示的字段，
 * 或者在设置该字段时保证它的值非空，这两种情况均不会抛校验异常！
 * <p>
 * 解决方案：
 * 在该过滤器中修改HTTP请求头，如果为空值就从请求头参数列表中移除这个字段！
 */
@Slf4j
public class WebSessionFilter implements WebFilter, Ordered {

    private FluxFilterProperty filterProperty;

    @NonNull
    @Override
    public Mono<Void> filter(@NonNull ServerWebExchange exchange, @NonNull WebFilterChain chain) {
        // 重构HTTP请求头
        ServerHttpRequest httpRequest = exchange.getRequest();
        ServerHttpRequest.Builder requestBuilder = httpRequest.mutate();

        // 请求头中表示`会话标识`的字段名
        String authTokenKey = StandardCacheKeyBuilder.getAuthTokenKey();
        // 请求头中的`会话标识`
        String accessToken = httpRequest.getHeaders().getFirst(authTokenKey);
        requestBuilder.headers(httpHeaders -> {
            if (httpHeaders.containsKey(authTokenKey)) {
                if (StringUtils.isEmpty(accessToken) || StringUtils.isBlank(accessToken)) {
                    // sessionId must not be empty
                    httpHeaders.remove(authTokenKey);
                }
            }
        });

        ServerHttpRequest newHttpRequest = requestBuilder.build();
        ServerWebExchange newExchange = exchange.mutate().request(newHttpRequest).build();


        // 当前HTTP请求的唯一标识
        final String requestId = exchange.getAttribute(ApplicationConstant.RESPONSE_IDENTITY);
        return sessionHandler(newExchange, authTokenKey, accessToken, requestId)
                .then(chain.filter(newExchange))
                .doOnTerminate(() -> log.debug("会话管理过滤器 - Web请求（{}）结束", requestId));

        // return chain.filter(exchange);
    }

    private Mono<WebSession> sessionHandler(ServerWebExchange exchange, String authTokenKey, String accessToken, String requestId) {
        return exchange.getSession()
                .flatMap(webSession -> {
                    // 获取发送HTTP请求的远程客户端设备标识
                    String clientIpAddress = exchange.getAttributeOrDefault(ApplicationConstant.REMOTE_CLIENT_IP_KEY, StringUtils.EMPTY);

                    if (webSession.isStarted() && webSession.getId().equals(accessToken)) {
                        String loginName = webSession.getAttributeOrDefault(SessionConstant.SESSION_LOGIN_KEY, StringUtils.EMPTY);
                        // 1、 记录当前会话对象映射的登录名【在spring-webflux会话管理模块DefaultSessionHandler.java中退出登录成功时获取】
                        exchange.getAttributes().putIfAbsent(SessionConstant.SESSION_LOGIN_KEY, loginName);

                        // 2、将会话标识设置到 ServerWebExchange 对象的属性列表中【在StandardCacheKeyBuilder.java中获取】
                        // 我们约定：如果当前会话对象存在就能够保证在ServerWebExchange.getAttributes()中保存的Access-Token一定存在！
                        exchange.getAttributes().putIfAbsent(authTokenKey, accessToken);

                        // 3、将当前HTTP请求的会话对象最大持续时长设置到 ServerWebExchange 对象的属性列表中【在ReactiveCacheHandler.java中获取】
                        exchange.getAttributes().putIfAbsent(ApplicationConstant.SESSION_DURATION_KEY, webSession.getMaxIdleTime().getSeconds());

                        // 4、刷新客户端IP与服务器端会话数据的绑定关系
                        log.debug("会话管理过滤器 - Web请求（{}）刷新会话缓存", requestId);
                        return Mono.when(
                                ReactorSessionManager.readClientUserProfile()
                                        .flatMap(flagManagedProfile -> ReactorSessionManager.refreshTerminalSession(loginName, clientIpAddress, flagManagedProfile)),
                                ReactorSessionManager.removeExpiredTerminalSession(loginName, SessionCachePathEnum.SESSION_CREDENTIALS_PATH),
                                ReactorSessionManager.removeExpiredTerminalSession(clientIpAddress, SessionCachePathEnum.SESSION_PROFILE_PATH)
                        ).thenReturn(webSession);
                    } else {
                        // 5、nothing to do.
                        log.debug("会话管理过滤器 - Web请求（{}）发起临时会话", requestId);
                        return Mono.just(webSession);
                    }
                });
    }

    @Override
    public int getOrder() {
        return filterProperty == null
                ? new FluxFilterProperty().getOrderWebSessionFilter()
                : filterProperty.getOrderWebSessionFilter();
    }

    @Autowired(required = false)
    public void setFluxFilterProperty(FluxFilterProperty filterProperty) {
        this.filterProperty = filterProperty;
    }

}
